Skip to content
0

知识 - 请求日志输出

前言

在 Spring Boot 应用中,实现接口请求日志记录功能,能够记录包括请求方法、接口路径及请求参数等核心信息,并提供灵活的开关配置。

实现逻辑是利用 AOP 切面在 Controller 进行切入,获取请求的数据并打印处理。

实现

依赖

xml
<dependencies>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.32</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>
</dependencies>

枚举

java
@Getter
@RequiredArgsConstructor
public enum RequestLogLevelEnum {
    /**
     * No logs.
     */
    NONE(0),

    /**
     * Logs request and response lines.
     *
     * <p>Example:
     * <pre>{@code
     * --> POST /greeting http/1.1 (3-byte body)
     *
     * <-- 200 OK (22ms, 6-byte body)
     * }</pre>
     */
    BASIC(1),

    /**
     * Logs request and response lines and their respective headers.
     *
     * <p>Example:
     * <pre>{@code
     * --> POST /greeting http/1.1
     * Host: example.com
     * Content-Type: plain/text
     * Content-Length: 3
     * --> END POST
     *
     * <-- 200 OK (22ms)
     * Content-Type: plain/text
     * Content-Length: 6
     * <-- END HTTP
     * }</pre>
     */
    HEADERS(2),

    /**
     * Logs request and response lines and their respective headers and bodies (if present).
     *
     * <p>Example:
     * <pre>{@code
     * --> POST /greeting http/1.1
     * Host: example.com
     * Content-Type: plain/text
     * Content-Length: 3
     *
     * Hi?
     * --> END POST
     *
     * <-- 200 OK (22ms)
     * Content-Type: plain/text
     * Content-Length: 6
     *
     * Hello!
     * <-- END HTTP
     * }</pre>
     */
    BODY(3);

    /**
     * 级别
     */
    private final int level;

    /**
     * 请求日志配置前缀
     */
    public static final String REQ_LOG_PROPS_PREFIX = "controller.log";
    /**
     * 控制台日志是否启用
     */
    public static final String CONSOLE_LOG_ENABLED_PROP = "controller.log.console.enabled";

    /**
     * 当前版本 小于和等于 比较的版本
     *
     * @param level LogLevel
     * @return 是否小于和等于
     */
    public boolean lte(RequestLogLevelEnum level) {
        return this.level <= level.level;
    }
}

配置项

java
@Getter
@Setter
@ConfigurationProperties(value = RequestLogLevelEnum.REQ_LOG_PROPS_PREFIX)
public class RequestLogProperties {
    /**
     * 日志级别配置,默认:BODY
     */
    private RequestLogLevelEnum level = RequestLogLevelEnum.BODY;
}

切面

java
@Slf4j
@Aspect
@Configuration
@AllArgsConstructor
@ConditionalOnClass(LogAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(value = RequestLogLevelEnum.REQ_LOG_PROPS_PREFIX + ".enabled", havingValue = "true", matchIfMissing = true)
public class RequestLogAspect {
    private final RequestLogProperties properties;

    /**
     * AOP 环切 控制器 R 返回值
     * Response:响应类
     *
     * @param point JoinPoint
     * @return Object
     * @throws Throwable 异常
     */
    @Around(
            "execution(!static cn.youngkbt.core.http.Response *(..)) && " +
                    "(@within(org.springframework.stereotype.Controller) || " +
                    "@within(org.springframework.web.bind.annotation.RestController))"
    )
    public Object aroundApi(ProceedingJoinPoint point) throws Throwable {
        RequestLogLevelEnum level = properties.getLevel();

        // 不打印日志,直接返回
        if (RequestLogLevelEnum.NONE == level) {
            return point.proceed();
        }

        HttpServletRequest request = WebUtil.getRequest();
        String requestUrl = Objects.requireNonNull(request).getRequestURI();
        String requestMethod = request.getMethod();

        // 构建成一条长 日志,避免并发下日志错乱
        StringBuilder beforeReqLog = new StringBuilder(300);
        // 日志参数
        List<Object> beforeReqArgs = new ArrayList<>();
        beforeReqLog.append("\n\n================  Request Start  ================\n");
        // 打印路由
        beforeReqLog.append("===> {}: {}");
        beforeReqArgs.add(requestMethod);
        beforeReqArgs.add(requestUrl);
        // 打印请求参数
        logIngArgs(point, beforeReqLog, beforeReqArgs);
        // 打印请求 headers
        logIngHeaders(request, level, beforeReqLog, beforeReqArgs);
        beforeReqLog.append("================   Request End   ================\n");

        // 打印执行时间
        long startNs = System.nanoTime();
        log.info(beforeReqLog.toString(), beforeReqArgs.toArray());
        // aop 执行后的日志
        StringBuilder afterReqLog = new StringBuilder(200);
        // 日志参数
        List<Object> afterReqArgs = new ArrayList<>();
        afterReqLog.append("\n\n================  Response Start  ================\n");
        try {
            Object result = point.proceed();
            // 打印返回结构体
            if (RequestLogLevelEnum.BODY.lte(level)) {
                afterReqLog.append("===Result===  {}\n");
                afterReqArgs.add(JacksonUtil.toJsonStr(result));
            }
            return result;
        } finally {
            long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
            afterReqLog.append("<=== {}: {} ({} ms)\n");
            afterReqArgs.add(requestMethod);
            afterReqArgs.add(requestUrl);
            afterReqArgs.add(tookMs);
            afterReqLog.append("================   Response End   ================\n");
            log.info(afterReqLog.toString(), afterReqArgs.toArray());
        }
    }

    /**
     * 激励请求参数
     *
     * @param point         ProceedingJoinPoint
     * @param beforeReqLog  StringBuilder
     * @param beforeReqArgs beforeReqArgs
     */
    public void logIngArgs(ProceedingJoinPoint point, StringBuilder beforeReqLog, List<Object> beforeReqArgs) {
        MethodSignature ms = (MethodSignature) point.getSignature();
        Method method = ms.getMethod();
        Object[] args = point.getArgs();
        // 请求参数处理
        final Map<String, Object> paraMap = new HashMap<>(16);
        // 一次请求只能有一个 request body
        Object requestBodyValue = null;
        for (int i = 0; i < args.length; i++) {
            // 读取方法参数
            MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
            // PathVariable 参数跳过
            PathVariable pathVariable = methodParam.getParameterAnnotation(PathVariable.class);
            if (pathVariable != null) {
                continue;
            }
            RequestBody requestBody = methodParam.getParameterAnnotation(RequestBody.class);
            String parameterName = methodParam.getParameterName();
            Object value = args[i];
            // 如果是body的json则是对象
            if (requestBody != null) {
                requestBodyValue = value;
                continue;
            }
            // 处理 参数
            if (value instanceof HttpServletRequest) {
                paraMap.putAll(((HttpServletRequest) value).getParameterMap());
                continue;
            } else if (value instanceof WebRequest) {
                paraMap.putAll(((WebRequest) value).getParameterMap());
                continue;
            } else if (value instanceof HttpServletResponse) {
                continue;
            } else if (value instanceof MultipartFile) {
                MultipartFile multipartFile = (MultipartFile) value;
                String name = multipartFile.getName();
                String fileName = multipartFile.getOriginalFilename();
                paraMap.put(name, fileName);
                continue;
            } else if (value instanceof List) {
                List<?> list = (List<?>) value;
                AtomicBoolean isSkip = new AtomicBoolean(false);
                for (Object o : list) {
                    if ("StandardMultipartFile".equalsIgnoreCase(o.getClass().getSimpleName())) {
                        isSkip.set(true);
                        break;
                    }
                }
                if (isSkip.get()) {
                    paraMap.put(parameterName, "此参数不能序列化为json");
                    continue;
                }
            }
            // 参数名
            RequestParam requestParam = methodParam.getParameterAnnotation(RequestParam.class);
            String paraName = parameterName;
            if (requestParam != null && StringUtil.hasText(requestParam.value())) {
                paraName = requestParam.value();
            }
            if (value == null) {
                paraMap.put(paraName, null);
            } else if (ClassUtil.isPrimitiveOrWrapper(value.getClass())) {
                paraMap.put(paraName, value);
            } else if (value instanceof InputStream) {
                paraMap.put(paraName, "InputStream");
            } else if (value instanceof InputStreamSource) {
                paraMap.put(paraName, "InputStreamSource");
            } else if (JacksonUtil.canSerialize(value)) {
                // 判断模型能被 json 序列化,则添加
                paraMap.put(paraName, value);
            } else {
                paraMap.put(paraName, "此参数不能序列化为json");
            }
        }
        // 请求参数
        if (paraMap.isEmpty()) {
            beforeReqLog.append("\n");
        } else {
            beforeReqLog.append(" Parameters: {}\n");
            beforeReqArgs.add(JacksonUtil.toJsonStr(paraMap));
        }
        if (requestBodyValue != null) {
            beforeReqLog.append("====Body=====  {}\n");
            beforeReqArgs.add(JacksonUtil.toJsonStr(requestBodyValue));
        }
    }

    /**
     * 记录请求头
     *
     * @param request       HttpServletRequest
     * @param level         日志级别
     * @param beforeReqLog  StringBuilder
     * @param beforeReqArgs beforeReqArgs
     */
    public void logIngHeaders(HttpServletRequest request, RequestLogLevelEnum level,
                              StringBuilder beforeReqLog, List<Object> beforeReqArgs) {
        // 打印请求头
        if (RequestLogLevelEnum.HEADERS.lte(level)) {
            Enumeration<String> headers = request.getHeaderNames();
            while (headers.hasMoreElements()) {
                String headerName = headers.nextElement();
                String headerValue = request.getHeader(headerName);
                beforeReqLog.append("===Headers===  {}: {}\n");
                beforeReqArgs.add(headerName);
                beforeReqArgs.add(headerValue);
            }
        }
    }
}

容器装配

主要加载 RequestLogProperties 配置类。

java
@AutoConfiguration
@EnableConfigurationProperties({RequestLogProperties.class})
public class LogAutoConfiguration {

}

Spring Boot 3.x 需要在 resource 下建立 META-INF/spring 路径,然后创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,内容为

java
cn.youngkbt.log.config.LogAutoConfiguration

这样 Spring 会自动扫描该文件的容器装配类,将里面涉及的类注入到 Spring 容器。

最近更新